import csv
import os
from copy import copy
from dataclasses import dataclass, field, asdict


@dataclass
class Item:
    id: str
    name: str
    price: float = field(default=0)
    quantity: int = field(default=0)

    def __post_init__(self):
        self.price = round(self.price, 2)  # ensure it has 2 decimal places

    def __str__(self):  # toString
        return f"Name: {self.name}, Price: RM{self.price:.2f}, Stock: {self.quantity}"

    def search(self, query: str):
        # compare id
        return self.id == query

    def merge(self, modified):
        # update the name if different from original
        self.name = modified.name if modified.name != "" else self.name
        self.price = round(modified.price, 2) if modified.price != "" else self.price
        self.quantity = modified.quantity if modified.quantity != "" else self.quantity


# Variables
DATA_PATH = "data"  # Folder to save
STORE_ITEM_DATA = os.path.join(DATA_PATH, "items.csv")
STORE_SALES_DATA = os.path.join(DATA_PATH, "sales.csv")

ITEM_HEADER = ["id", "name", "price", "quantity"]
SALES_HEADER = ["id", "name", "quantity sold", "total price"]

TAX_PERCENT = 6

store_items = []  # [Item(), Item(), ...]
store_sales = {}  # {item1: 10, item2: 2}
transaction_no = 1


def main():
    load_file()
    logo()
    while True:
        print(f"{'Transaction Menu':-^30}")
        print("1. New Sales")
        print("2. View Total Sales")
        print(f"{'Items Menu':-^30}")
        print("3. Add Item")
        print("4. Modify Item")
        print("5. Delete Item")
        print("6. List All Items")
        print(f"{'':-^30}")
        print("7. Exit The Program")
        selection = parse_input("Your selection: ", int)
        if selection == 1:
            process_sale()
        elif selection == 2:
            view_total_sale(store_sales)
        elif selection == 3:
            add_item(store_items)
        elif selection == 4:
            modify_item(store_items)
        elif selection == 5:
            delete_item(store_items)
        elif selection == 6:
            list_item(store_items)
        elif selection == 7:
            break
        else:
            print("Your selection is not valid. Please try again")
    save_file()
    print("Thank you for using this program")


def logo():
    print("\
 _____     ____  _        _   _                                    \n\
| ____|   / ___|| |_ __ _| |_(_) ___  _ __   ___ _ __ _   _        \n\
|  _| ____\\___ \\| __/ _` | __| |/ _ \\| '_ \\ / _ \\ '__| | | |  \n\
| |__|_____|__) | || (_| | |_| | (_) | | | |  __/ |  | |_| |       \n\
|_____|   |____/ \\__\\__,_|\\__|_|\\___/|_| |_|\\___|_|   \\__, | \n\
                                                      |___/        ")


# Item Sales
def process_sale():
    """Transaction starts here"""
    global store_items
    global store_sales
    global transaction_no

    transaction_sale: dict[str: int] = {}  # per new sales
    total_price = 0

    # start of transaction
    list_menu(store_items)
    print(f"Transaction No: {transaction_no}")

    perform_sale = True
    while perform_sale:
        item_id = parse_input("Enter the id ([pay]ment, [q]uit): ", str)

        if item_id.lower() == 'pay' and transaction_sale:
            perform_sale = False
            payment(transaction_sale, total_price)

            for item_id, qty in transaction_sale.items():
                store_sales.setdefault(item_id, 0)  # create key, if item not exist
                store_sales[item_id] += qty  # update global total per item sold

                item = [i for i in store_items if i.id == item_id][0]  # get item obj
                item_index = store_items.index(item)  # get target item index
                store_items[item_index].quantity -= qty  # decrease stock of item

            transaction_no += 1
            continue  # back to menu
        elif item_id.lower() == 'pay' and transaction_sale == {}:  # empty
            print("You did not have any transaction.")
            continue  # perform another transaction
        elif item_id.lower() == 'q':
            perform_sale = False
            print("Quiting transaction. Returning to menu.")
            continue

        # Searching item
        for item in store_items:
            if item.search(item_id):
                break
        else:
            item = None  # item not in store

        if item is not None and item.quantity > 0:
            quantity = max(0, parse_input("Quantity: ", int))

            if quantity > item.quantity:
                print("Item bought greater than in stock. Please retry.")
                continue

            transaction_sale[item.id] = quantity  # add item id and quantity

            price = item.price * quantity  # item total price
            print(f"Item: {item.name} x {quantity} = RM{price:.2f}")
            total_price += price
        elif item is not None and item.quantity <= 0:
            print("Item is out of stock. Please select another item")
        else:
            print("Item not found. Please enter again")


def list_menu(items):
    """List all available store items"""
    print(f"{' Item List ':=^60}")
    print(f"|{'ID':^15}|{'Name':^30}|{'Price(RM)':^11}")
    print(f"{'':=^60}")
    for item in items:
        if item.quantity > 0:
            print(f" {item.id:15} {item.name:30} {item.price:9.2f}")
    print(f"{'':=<60}")


def payment(sales: dict, total_price: float):
    """ sales: customer bought items to pay"""
    tax_charge = round(total_price * (TAX_PERCENT / 100), 2)
    grand_total = total_price + tax_charge

    view_total_sale(sales)  # show items that bought by customer
    print(f"{'Total':>56}: {total_price:9.2f}")
    print(f"{'Tax Charged':>51} - {TAX_PERCENT}%: {tax_charge:9.2f}")
    print(f"{'Grand Total':>56}: {grand_total:9.2f}")

    while True:
        payment_receive = round(parse_input(f"{'Payment':>56}:    ", float), 2)
        if payment_receive >= grand_total:
            changes = payment_receive - grand_total
            print(f"{'':>57} {'':=>11}")
            print(f"{'Changes':>56}: {changes:9.2f}")
            print(f"{'':>57} {'':=>11}")
            return
        else:
            print("Insufficient payment")


def view_total_sale(sales: dict):
    """View the total sales performed"""
    print(f"{' Total Item Sales ':=^70}")
    print(f"|{'ID':^14}|{'Name':^30}|{'Qty Sold':^10}|{'Total(RM)':^11}|")
    print(f"{'':=<70}")
    ids = None
    for ids, qty in sales.items():
        # We need to extract the Item object since list comprehension return list
        # and we want only the object.
        item = [i for i in store_items if i.id == ids][0]
        print(f" {item.id:14} {item.name:30} {qty:10} {item.price * qty:9.2f}")
    if ids is None:
        print(f"{'No sales data':^70}")
    print(f"{'':=<70}")


# Item Managing
def add_item(store):
    while True:
        print("What item you need to add?")
        item_id = parse_input("ID: ", str)
        item_exists = any(item.id for item in store if item.id == item_id)

        if item_exists:
            print(f"Item {item_id} exists in store!")
        elif item_id != '':
            # continue add item details
            item_name = parse_input("Name: ", str)
            item_price = parse_input("Price: ", float)
            item_qty = parse_input("Quantity: ", int)
            store.append(Item(item_id, item_name, item_price, item_qty))
            print(f"Item {item_name} has added successfully!")
            print()
        else:
            print("ID cannot be empty.")

        while True:
            result = parse_input("Continue Adding? (Y/N): ", str)
            if result.lower() == 'n':
                return
            elif result.lower() == 'y':
                break
            else:
                print("Please try again.")


def modify_item(store):
    while True:
        list_item(store)
        print("Choose which Item to edit. (q to quit)")
        selection = parse_input("Item ID to edit: ", str)

        if selection.lower() == "q":
            return

        for i, item in enumerate(store):
            if item.search(selection):
                break
        else:  # item not exists
            i = None
            item = None

        if item is not None:
            print(f"Editing ID: {item.id} - {item}")
            print("Press [Enter] to skip changes")
            item_edit = copy(item)  # prevent original item change
            item_edit.name = parse_input("New name: ", str, True)
            item_edit.price = parse_input("New Price (RM): ", float, True)
            item_edit.quantity = parse_input("New Quantity: ", int, True)

            item.merge(item_edit)
            # to remove the item and replace with the updated one at same index
            store.pop(i)
            store.insert(i, item)
            print("Item successfully edited!")
        else:
            print(f"Item not found!")

        while True:
            result = parse_input("Continue Editing? (Y/N): ", str)
            if result.lower() == 'n':
                return
            elif result.lower() == 'y':
                break
            else:
                print("Please try again.")


def delete_item(store):
    while True:
        list_item(store)
        print("Choose which Item to delete. (q to quit)")
        selection = parse_input("Item ID to delete: ", str)

        if selection.lower() == "q":
            return

        for i, item in enumerate(store):
            if item.search(selection):
                break
        else:  # item not exist
            i = None
            item = None

        if item is not None:
            print(f"Deleting ID: {item.id} - {item}")
            while True:
                result = parse_input("Confirm delete item? (y/n): ", str)
                if result.lower() == 'n':
                    break
                elif result.lower() == 'y':
                    store.pop(i)
                    print("Item successfully deleted!")
                    break
                else:
                    print("Please try again.")
        else:
            print("Item not found!")


def list_item(items):
    """List all available store items"""
    print(f"{' Item List ':=^70}")
    print(f"|{'ID':^14}|{'Name':^30}|{'Price(RM)':^11}|{'Quantity':^10}|")
    print(f"{'':=<70}")
    for item in items:
        print(f" {item.id:14} {item.name:30} {item.price:9.2f} {item.quantity:10}")
    print(f"{'':=<70}")


# Utilities
def load_file():
    try:
        with open(STORE_ITEM_DATA, 'r', newline='') as item_file:
            item_file_reader = csv.DictReader(item_file, ITEM_HEADER)
            next(item_file_reader)  # skip header
            for row in item_file_reader:
                id_ = row["id"]
                name = row["name"]
                price = float(row["price"])
                quantity = int(row["quantity"])
                store_items.append(Item(id_, name, price, quantity))
    except FileNotFoundError:
        pass


def save_file():
    if not os.path.exists(DATA_PATH):
        os.makedirs(DATA_PATH)  # data folder not exists

    with open(STORE_ITEM_DATA, "w+", newline='') as item_file:
        item_file_writer = csv.DictWriter(item_file, ITEM_HEADER)
        item_file_writer.writeheader()
        for item in store_items:
            item_file_writer.writerow(asdict(item))

    with open(STORE_SALES_DATA, "w+", newline='') as sales_file:
        sales_file_writer = csv.writer(sales_file)
        sales_file_writer.writerow(SALES_HEADER)
        for ids, sale_qty in store_sales.items():
            # we only need one result (item)
            item = [i for i in store_items if i.id == ids][0]
            row = [item.id, item.name, sale_qty, (item.price * sale_qty)]
            sales_file_writer.writerow(row)


def parse_input(prompt: str, target_type, allow_skip=False):
    """Handles user input with type conversion"""
    while True:
        try:
            result = input(prompt)
            if allow_skip and result.strip() == '':
                return ''
            result = target_type(result)  # type conversion
        except ValueError:
            print("Invalid input. Please try again.")
        else:
            return result


if __name__ == '__main__':
    # execute main when run
    main()
